%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;---------------------------------------------------------构建GDT--------------------------------------------------------
;构建gdt及其内部描述符
    GDT_BASE:   dd 0x00000000
                dd 0x00000000

    CODE_DESC:  dd 0x0000FFFF       ;低32位
                dd DESC_CODE_HIGH4  ;高32位,与书上的顺序解释不同,我觉得这么定义是小端字节序的原因
    
    DATA_STACK_DESC:    dd 0x0000FFFF
                        dd DESC_DATA_HIGH4
    
    VIDEO_DESC: dd 0x80000007;limit=(0xbffff-0xb8000)/4k=0x7
                dd DESC_VIDEO_HIGH4;此时dpl为0

    GDT_SIZE equ $ - GDT_BASE
    GDT_LIMIT equ GDT_SIZE - 1
    times 60 dq 0 ;预留60个描述符空位
    SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
    
    ; total_mem_bytes 用于保存内存容量，以字节为单位，此位置比较好记
    ; 当前偏移 loader.bin 文件头 0x200 字节; loader.bin 的加载地址是 0x900 
    ; 故 total_mem_bytes 内存中的地址是 0xb00 ; 将来在内核中咱们会引用此地址
    total_mem_bytes dd 0 
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 
 
    ;以下是定义 gdt 的指针，前 2 字节是 gdt 界限，后 4 字节是 gdt 起始地址
    gdt_ptr dw GDT_LIMIT ;2字节 word
    dd GDT_BASE         ;4字节 dword
  
    ;  人工对齐:total_mem_bytes4+gdt_ptr6+ards_buf244+ards_nr2，共 256 字节
    ards_buf times 244 db 0 
    ards_nr dw 0 ;用于记录 ARDS 结构体数量

loader_start:
;---------------------------------------------------------获取内存布局--------------------------------------------------------
 ; int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局

    xor ebx, ebx    ;第一次调用时，ebx 值要为 0
    mov edx, 0x534d4150 ;edx 只赋值一次，循环体中不会改变
    mov di, ards_buf    ;ards 结构缓冲区
.e820_mem_get_loop:     ;循环获取每个 ARDS 内存范围描述结构
    mov eax, 0x0000e820 ;执行 int 0x15 后，eax 值变为 0x534d4150，;所以每次执行 int 前都要更新为子功能号
    mov ecx, 20         ;ARDS 地址范围描述符结构大小是 20 字节
    int 0x15
    jc .e820_failed_so_try_e801 ;若 cf 位为 1 则有错误发生，尝试 0xe801 子功能
    add di, cx          ;使 di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
    inc word [ards_nr]  ;记录 ARDS 数量
    cmp ebx, 0          ;若 ebx 为 0 且 cf 不为 1，这说明 ards 全部返回
    jnz .e820_mem_get_loop

     ;在所有 ards 结构中 ;找出(base_add_low + length_low)的最大值，即内存的容量
    mov cx, [ards_nr]   ;遍历每一个 ARDS 结构体,循环次数是 ARDS 的数量
    mov ebx, ards_buf
    xor edx, edx        ;edx 为最大的内存容量，在此先清 0
 .find_max_mem_area:;无需判断 type 是否为 1,最大的内存块一定是可被使用的
    mov eax, [ebx]      ;base_add_low
    add eax, [ebx+8]    ;length_low
    add ebx, 20         ;指向缓冲区中下一个 ARDS 结构
    cmp edx, eax        ;冒泡排序,找出最大,edx 寄存器始终是最大的内存容量
    jge .next_ards      ;大于等于跳转指令jge
    mov edx, eax        ;edx为总内存大小
.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小，最大支持 4G ------ 
; 返回后, ax cx 值一样,以 KB 为单位，bx dx 值一样，以 64KB 为单位
; 在 ax 和 cx 寄存器中为低 16MB，在 bx 和 dx 寄存器中为 16MB 到 4GB
.e820_failed_so_try_e801:
    mov ax, 0xe801
    int 0x15
    jc .e801_failed_so_try88    ;CF为1,表示调用出错,jc跳转

;1 先算出低15MB的内存   ; ax 和 cx 中是以 KB 为单位的内存数量，将其转换为以 byte 为单位
    mov cx, 0x400               ;cx 和 ax值一样,cx用作乘数
    mul cx                      ;cx*ax -> [dx,ax]
    shl edx, 16                 ;edx左移, 乘法结果的高16位存入edx的高16位
    and eax, 0x0000FFFF         
    or edx,eax                  ;edx高16与ax组合,组成完整的32位乘法结果
    add edx, 0x100000           ;ax 只是 15MB，故要加 1MB
    mov esi, edx                ;先把低 15MB 的内存容量存入 esi 寄存器备份

;2 再将 16MB 以上的内存转换为 byte 为单位; 寄存器 bx 和 dx 中是以 64KB 为单位的内存数量
    xor eax,eax
    mov ax, bx
    mov ecx, 0x10000    ;64K
    mul ecx     ;ecx*eax->[edx,eax]
    add esi, eax;由于此方法只能测出 4GB 以内的内存，故 32 位 eax 足够了; edx 肯定为 0，只加 eax 便可
    mov edx, esi    ;edx为总内存大小
    jmp .mem_get_ok

     ;----- int 15h ah = 0x88 获取内存大小，只能获取 64MB 之内 -----
.e801_failed_so_try88:
    ;int 15 后，ax 存入的是以 KB 为单位的内存容量
    mov ah, 0x88
    int 0x15
    ;jc .error_hlt
    and eax, 0x0000FFFF

    ;16 位乘法，被乘数是 ax，积为 32 位。积的高 16 位在 dx 中 ;积的低 16 位在 ax 中
    mov cx, 0x400
    mul cx
    shl edx, 16
    or edx, eax
    add edx, 0x100000   ;0x88 子功能只会返回 1MB 以上的内存;故实际内存大小要加上 1MB

.mem_get_ok:
    mov [total_mem_bytes], edx;将内存换为 byte 单位后存入 total_mem_bytes 处
;---------------------------------------------------------准备进入保护模式--------------------------------------------------------
;---------准备进入保护模式-----------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1


    ;----------打开A20-----------
    in al, 0x92
    or al, 0000_0010b
    out 0x92, al

    ;---------加载gdt-----------
    lgdt [gdt_ptr]

    ;---------cr0第0位置1----------
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线,防止cpu将后续32位指令译码为16位指令
;---------------------------------------------------------保护模式--------------------------------------------------------
;---------------------------------------------------------保护模式--------------------------------------------------------
[bits 32]
p_mode_start:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

;---------------------------------------------------------加载kernel--------------------------------------------------------  
    mov eax, KERNEL_START_SECTOR    ;kernel.bin 所在的扇区号
    mov ebx, KERNEL_BIN_BASE_ADDR   ;kernel.bin 需要被加载到的内存位置
    mov ecx, 500                    ;读入的扇区数

    call rd_disk_m_32               ;加载kernel

;---------------------------------------------创建页目录和页表并初始化页内存位图-------------------------------------------------
call setup_page

;要将描述符表地址及偏移量写入gdt_ptr,一会儿用新地址重新加载
sgdt [gdt_ptr];存储到原来gdt所有的位置

;将gdt描述符中视频段描述符的段地址+0xc0000000
mov ebx, [gdt_ptr + 2]
or dword [ebx + 0x18 + 4], 0xc0000000   ;视频段是第3个段描述符.每个描述符8字节,故0x18=24,再+4是取其高四字节进行或运算
;段描述符的高4字节的最高位是段地址的第31-24位, dword是双字,4字节

;将gdt的基址加上0xc0000000成为内核所在高地址
add dword [gdt_ptr + 2], 0xc0000000

add esp, 0xc0000000 ;栈指针同样映射到内核地址

;把页目录地址赋值给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

;打开cr0的pg位(31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr]  ;重新加载

; mov byte [gs:160], 'V'
jmp SELECTOR_CODE:enter_kernel  ;刷新流水线

;-------------------------------------------------------------进入内核-------------------------------------------------------
enter_kernel:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; mov byte [gs:162], 'K'
    call kernel_init
    mov esp, 0xc009f000
    
    jmp KERNEL_ENTRY_POINT  ;跳转内核





;========================================================================================================================
;=========================================================函数部分========================================================
;========================================================================================================================

;--------------------------------------------------创建页目录及页表(函数)---------------------------------------------------
setup_page:
;先把页目录占用的空间逐字节清零
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

;开始创建页表目录(PDE)
.create_pde:;create Page Directory Entry
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000     ;此时eax为第一个页表位置及属性
    mov ebx, eax        ;此处为ebx赋值,是为.create_pte做准备,ebx为基址

;下面将页目录项0和0xc00都作为第一个页表的地址,每个页表表示4MB内存,(loader在未启用分页时,要求线性地址与分页后的虚拟地址对应的物理地址一致)
;这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
;这是为将地址映射为内核地址做准备
    or eax, PG_US_U | PG_RW_W | PG_P    ;页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
    mov [PAGE_DIR_TABLE_POS + 0x0], eax ;第一个目录项   在目录表第一个目录项位置,写入第一个页表的位置0x101000及其属性
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax   ;0xc00是第768个页表占用的目录项,0xc00以上的目录项属于内核空间;也就是页表的0xc00_00000~0xfff_fffff共1G内存属于内核
    sub eax, 0x1000
    mov [PAGE_DIR_TABLE_POS + 4092], eax;使最后一个目录项指向,页目录表表自己的地址

;下面创建页表项(PTE)
    mov ecx, 256
    mov esi, 0      ;1M低端内存/每页大小4k =256
    mov edx, PG_US_U | PG_RW_W | PG_P   ;7
.create_pte:;create Page Table Entry
    mov [ebx + esi*4], edx  ;ebx = 0x101000 第一个页表的地址
    add edx, 4096
    inc esi
    loop .create_pte

;创建内核其他页表的PDE
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000     ;eax为第二个页表的位置
    or eax, PG_US_U | PG_RW_W | PG_P    ;7
    mov ebx, PAGE_DIR_TABLE_POS
    mov ecx, 254    ;768+1 ~ 1023-1的目录项的数量
    mov esi, 769    ;768+1
.create_kernel_pde:
    mov [ebx+esi*4], eax    ;给0x100000 + 0x769*4 (即目录中的第769,770,771...个目录项赋值为0x102000,0x103000,0x104000...此为对应的第769,770,771...个页表的内存位置)
    inc esi
    add eax, 0x1000
    loop .create_kernel_pde
    ret ;setup_page 函数结束


;------------------------------------------将kernel.bin中的segment拷贝到编译的地址------------------------------------------
kernel_init:
    xor eax, eax
    xor ebx, ebx    ;ebx记录程序头表地址
    xor ecx, ecx    ;cx记录头表中的program header数量
    xor edx, edx    ;dx记录program header表的条目尺寸,即e_phentsize

    mov dx, [KERNEL_BIN_BASE_ADDR + 42] ;偏移文件42字节处的属性是e_phentsize,表示program header大小(每个条目的描述信息尺寸)
    mov ebx, [KERNEL_BIN_BASE_ADDR + 28];偏移文件28字节处的属性是e_phoff,表示第一个program header在文件中的偏移量

    add ebx, KERNEL_BIN_BASE_ADDR   ;给ebx加上文件加载到的地址,作为程序头表的真正物理地址
    mov cx, [KERNEL_BIN_BASE_ADDR + 44] ;偏移文件44字节处的属性是e_phnum,表示program header的个数

.each_segement:
    cmp byte [ebx + 0], PT_NULL ;若p_type = PT_NULL(0), 说明此program header 未使用 ;cmp比较两值,若相等则置ZF=1
    je .PTNULL  ;[ebx+0]等于PT_NULL , 即ZF=1时就跳转
    
    ;--为函数memcpy压入参数,从右往左---
    ;memcpy(dst, src, size)
    push dword [ebx + 16]   ;program header中偏移16字节的地方是p_filesz,本段在文件中的字节大小;  压入函数 memcpy 的第三个参数：size , dword为双字,即4字节
    mov eax, [ebx + 4]      ;距离程序头4字节的位置:p_offset,此成员给出从文件头到该段第一个字节的偏移
    add eax, KERNEL_BIN_BASE_ADDR   ;加上   kernel.bin 被加载到的物理地址，eax 为该段的物理地址
    push eax                ;压入函数 memcpy 的第二个参数：源地址
    push dword [ebx + 8]    ;压入函数 memcpy 的第一个参数：目的地址, p_vaddr, 偏离程序头8字节

    call mem_cpy    ;调用,完成复制

    add esp, 12     ;清理栈中的三个参数
.PTNULL:
    add ebx, edx    ; edx 为 program header 大小，即 e_phentsize    ;在此 ebx 指向下一个 program header
    loop .each_segement
    ret ;kernel_init 函数结束

;-------------------------------------------- 逐字节拷贝 mem_cpy（dst，src，size） ----------------------------------------
;输入:栈中三个参数（dst，src，size）
;输出:无
;---------------------------------------------------------
mem_cpy:
    cld ;clean direction 每搬运一次,cpu会给esi,edi加1(movsb:1Byte)
    push ebp
    mov ebp, esp
    push ecx        ; rep指令用到了ecx, 但ecx对于外层段的循环还有用,入栈备份
    mov edi, [ebp + 8]  ;dst
    mov esi, [ebp + 12] ;src
    mov ecx, [ebp + 16] ;size
    rep movsb       ;逐字节(byte)拷贝
                    ;按照ecx的次数,重复执行movsb,movsb是将DS:(E)SI所表示的地址处的一个字节,搬运到ES:(E)DI所表示的地址处
    ;恢复环境
    pop ecx
    pop ebp
    ret ;mem_cpy 函数结束


;-----------------------------------------------------保护模式下读取硬盘--------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_32:;在32位模式下读硬盘
;----------------
    mov esi,    eax ;备份eax, al在out中会用到,会影响到低8位的ax寄存器
    mov di,     cx  ;备份ecx

;读写硬盘
;1.设置要读取的扇区数
    mov dx, 0x01f2  ;dx存储0x01f2这个端口号,见表3-17,p126页,是sector count寄存器
    mov al, cl
    out dx, al  ;读取的扇区数,把al写入dx所存储的端口号对应的端口中去

    mov eax,    esi ;恢复ax

;2.将LBA的地址存入0x1f3~0x1f6

    ;7~0位写入0x1f3 LBA LOW
    mov dx, 0x01f3
    out dx, al

    ;15~8
    mov dx, 0x01f4
    mov cl, 8
    shr eax, cl ;逻辑右移
    out dx, al

    ;23~16
    mov dx, 0x01f5
    shr eax, cl
    out dx, al

    shr eax, cl
    and al, 0x0f ;24~27位
    or al,  0xe0 ;设置7-4位为1110,表示lba模式
    mov dx, 0x01f6 ;device
    out dx, al

;3.向0x1f7端口写入读命令,0x20
    mov dx, 0x01f7 ;command
    mov al, 0x20
    out dx, al ;写入后,硬盘开始工作了

;4.检测硬盘状态
.not_ready:
    ;同一端口,写入时表示写入命令字,读时表示读入硬盘状态
    nop ;空操作, 为了增加延迟, 相当于小sleep
    in al, dx ;读取0x1f7端口给出的硬盘状态
    and al,0x88 ; 第三位为1表示硬盘准备好传输数据
                ; 第7位为1表示硬盘忙
                ; 0x88 = 10001000
    cmp al,0x08 ; 减法,判断3位是否为1,al-0x08
                ; 减法操作会影响ZF,CF,PF等, 借助ZF判断cmp结果
    jnz .not_ready  ;0x08 - 0x08 = 0 -> ZF = 1
                    ;0x08 - 0x80 != 0 -> ZF = 0
                    ;jnz: 若ZF=0,即减法结果不为0 再重新检测,等待

;5.从0x1f0端口读取数据,data寄存器是16位,每次in只能读入2字节,根据读入数据总量(扇区数*512字节)决定in指令次数

    mov ax, di
    mov dx, 256
    mul dx ;将256与ax=di=待读入的扇区数相乘,得到in操作次数
    mov cx, ax ;ecx为循环次数,对应loop
    mov dx, 0x01f0 ;DATA端口

.go_on_read:
    in ax, dx
    mov [ds:ebx], ax
    add ebx, 2
    loop .go_on_read
    ret ;对应之前的call,返回之前call的位置